// a container for loading and interacting database... 
CE.MEDIA = {

    init: function () {
        CE.WIDGETS.SLIDER.toggle.on({
            cb: this.refresh,
            when: 'after'
        });
        
        CE.MEDIA.polygonTool = new OpenLayers.Control.DrawFeature(CE.user_shape_layer, OpenLayers.Handler.Polygon, {
            'featureAdded': function(feature) {
                CE.MEDIA.current_features.push(feature);
                var wkt_maker = new OpenLayers.Format.WKT({
                    'internalProjection': new OpenLayers.Projection("EPSG:900913"),
                    'externalProjection': new OpenLayers.Projection("EPSG:4326")
                });
                var point = feature.geometry.getCentroid().transform(new OpenLayers.Projection("EPSG:900913"), new OpenLayers.Projection("EPSG:4326"));
                var shape_wkt = wkt_maker.write(feature);
                CE.MEDIA.ADD.$iframe.contents().find('input.draw').val(shape_wkt);
                CE.MEDIA.ADD.$iframe.contents().find('input[name="latitude"]').val(point.y);
                CE.MEDIA.ADD.$iframe.contents().find('input[name="longitude"]').val(point.x);
                $('#ce_dialog').show();
                CE.MEDIA.polygonTool.deactivate();
            }
        });

        CE.MEDIA.deactivateAllDrawTool = function() {
            CE.controls.draw_tool.deactivate();
            CE.MEDIA.polygonTool.deactivate();
            CE.MEDIA.chooseMode = false;
        };

        CE.map.addControl(CE.MEDIA.polygonTool);

    },

    getIcon: function (feature) {
        return "/media/img/" + feature.ce.type.name + "-icon.png";
    },

    features: [],

    //non-spatial media.
    nsp_features: [],
    /* gen_point_feature
     *  a simple generator for OpenLayers point features.
     *
     *  options:
     *    object opts.marker_styles - styles to pass directly to the marker constructor.
     *    OpenLayers.Geometry.Point opts.pos - the position
     *    object opts.ce - a container for custom properties to attach to the feature.
     */
    gen_point_feature: function (opts) {

        var vec = {},
            match;
        opts = opts || {};
        opts = $.extend({
            marker_styles: {},
            ce: {}
        }, opts);

        opts.ce.type = opts.ce.type || CE.media_types[opts.ce.type_id];
        delete opts.ce.type_id;

        if (opts.pos) { // spatial feature
            vec = new OpenLayers.Feature.Vector(opts.pos, null, $.extend({}, OpenLayers.Feature.Vector.style['default'], {
                graphicWidth: 16,
                graphicYOffset: -8,
                graphicOpacity: 1,
                externalGraphic: this.getIcon(opts)
            }, opts.marker_styles));
            opts.ce.label_id = "#label_" + vec.id.replace(/\./g, "-");
        } else { // non-spatial feature.
            vec = {
                id: CE.getGuid(),
                is_nsp: true
            };
        }

        vec.ce = opts.ce;
        // it's possible to only define the start year.
        vec.ce.start_year = CE.UTIL.date2year(vec.ce.date_start);
        vec.ce.end_year = CE.UTIL.date2year(vec.ce.date_end) || vec.ce.start_year;

        // cache thumbnail src of photos.
        if (vec.ce.type.name === 'photo') {
            if (vec.ce.content.indexOf('http://www.flickr.com/apps/slideshow/show.swf') != -1 || vec.ce.content.indexOf('http://picasaweb.google.com/s/c/bin/slideshow.swf') != -1) {
                vec.ce.src = '/media/img/photo-album-icon.png';
            } else {
                match = vec.ce.content.match(/src=["']([^'"]+)["']/);
                if (match && match.length) vec.ce.src = match[1];
            }
        }

        return vec;
    },

    // loads all media features from the database    
    load: function () {
        var feature, features, start_year, end_year, i, ce;
        $.getJSON("/get_media", function (data) {
            var buckets = {};
            // loop through each media feature
            for (var i in data) {
                
                if(data[i].user_shapes)
                {
                    json = data[i].user_shapes.geoms;
                    var ce = $.extend({}, data[i]);
                    ce.style = eval('(' + data[i].user_shapes.style + ')');
                    ce.type = ce.type || CE.media_types[ce.type_id];
                    ce.shape_or_point = 'shape';
                    if (json.length)
                        CE.MEDIA.ADD.render_json(json, ce);
                }
                
                // non-spatial media can put in a separate media list (no composite analysis)                  
                if (data[i].latitude === "None") {
                    CE.MEDIA.nsp_features.push(CE.MEDIA.gen_point_feature({
                        ce: data[i]
                    }));
                    continue;
                }
   
                var pointce = $.extend({}, data[i]);
                pointce.shape_or_point = 'point';
                feature = CE.MEDIA.gen_point_feature({
                    // get the location, type, title and time values
                    pos: CE.UTIL.point(data[i].longitude, data[i].latitude),
                    ce: pointce
                });
  
                CE.MEDIA.features.push(feature);
                CE.media_layer.addFeatures([feature]);
                
                CE.media_layer.redraw();
                CE.MEDIA.list.draw();
                CE.WIDGETS.SLIDER.markers.refresh();
                CE.LAYERS.refresh();
            }

            CE.MEDIA.update_all()
            $("#dialog").dialog('close');
        });

        $(document).ready(function () {
            $("#dialog").dialog({disabled: false, modal: true});
        });

    },

    // the media search button handler.
    search: function () {
        var expr = new RegExp($('#search_box').val() || ".", "i");
        $('.feature-list').children().each(function (i, item) {
            feature = CE.MEDIA.get_feature_by_label(this.id);
            found = expr.test($(item).text()) || (feature && expr.test(feature.ce.title));
            if (found) {
                $(item).show();
            } else {
                $(item).hide();
            }
        });
    },

    // Add new media dialog.
    // uses an iframe in order to upload files to the server.
    ADD: {

        get_frame_body: function () {
            return this.$iframe.contents().find('body');
        },

        defer_dialog: function () {
            setTimeout(function () {
                CE.MEDIA.ADD.dialog(false);
            }, 200);
        },

        // Dialog for adding new media or uploading new shapefile to the current theme.
        // media = true indicates adding new media, o.w. indicates uploading new shapefile
        dialog: function (media) {

            // get a point from the canvas.
            CE.controls.draw_tool.activate();
            CE.MEDIA.current_features = [];
            var iframe = document.createElement('iframe');
            // override sucky IE iframe defaults.
            iframe.frameBorder = 0;
            iframe.frameMargin = 0;
            iframe.framePadding = 0;

            var afterload = this.load_cb,
                height = "380px",
                title = "Add Media";

            this.$iframe = $(iframe).css({
                width: "100%",
                height: height
            }).load(afterload);

            CE.WIDGETS.DIALOG.draw({
                title: title,
                width: 600,
                content: this.$iframe
            });

            setTimeout('CE.MEDIA.ADD.media_form(false)', 500);

        },

        // Called when the iframe body loads.
        // This is done to trigger an action when we receive messages in the iframe body from the server.
        load_cb: function (point) {
            CE.user_shape_layer.removeFeatures(CE.MEDIA.current_features);
            if (CE.MEDIA.selectedFeature)
                CE.MEDIA.selectedFeature.style.fillColor = CE.MEDIA.selectedFeatureFillColor;
            CE.MEDIA.current_features = []
            var $body = CE.MEDIA.ADD.get_frame_body();

            // get the data element.
            var $json = $body.find('.json');
            if ($json.length) {
                var rsp = CE.UTIL.parse_json($json.html().replace('"\\&quot;','\'').replace('\\&quot;"', '\''));
                if (rsp.media_item.user_shapes)
                {
                    json = rsp.media_item.user_shapes.geoms;
                    var ce = $.extend({}, rsp.media_item);
                    ce.style = eval('(' + rsp.media_item.user_shapes.style + ')');
                    ce.type = ce.type || CE.media_types[ce.type_id];
                    ce.shape_or_point = 'shape';
                    if (json.length)
                        CE.MEDIA.ADD.render_json(json, ce);
                }
                
                var pointce = $.extend({}, rsp.media_item);
                pointce.shape_or_point = 'point';
                feature = CE.MEDIA.gen_point_feature({
                    pos: CE.UTIL.point(rsp.media_item.longitude, rsp.media_item.latitude),
                    ce: pointce
                });
                CE.MEDIA.features.push(feature);
                CE.media_layer.addFeatures([feature]);
                
                CE.media_layer.redraw();
                CE.MEDIA.list.draw();
                CE.WIDGETS.SLIDER.markers.refresh();
                //$body.html("<b>successfully created media.</b>")
                if (CE.MEDIA.ADD.add_another) {
                    CE.MEDIA.ADD.form(true);
                } else if (rsp.success) {
                    CE.WIDGETS.DIALOG.close();
                }
            }
        },

        goog: new OpenLayers.Projection("EPSG:900913"),
        ll: new OpenLayers.Projection("EPSG:4326"),
        reproject: function (feature) {
            var geo = feature.geometry.clone();
            geo.transform(CE.MEDIA.ADD.ll, CE.MEDIA.ADD.goog);
            feature.geometry = geo;
        },

        render_json: function (jsonstr, ce) {
            var geojson_format = new OpenLayers.Format.GeoJSON();
            jsonstr = jsonstr.substring(jsonstr.indexOf('{'));
            shape_jsons = eval('(' + jsonstr + ')');
            for (var j = 0; j < shape_jsons.features.length; j++) {
                json = JSON.stringify(shape_jsons.features[j]);
                vecs = geojson_format.read(json);
                if (vecs.length == 0) continue;
                for (var i = 0; i < vecs.length; i++) {
                    CE.MEDIA.ADD.reproject(vecs[i]);
                    var style = ce.style;
                    var layer_style = OpenLayers.Util.extend({}, OpenLayers.Feature.Vector.style['default']);
                    layer_style.fillOpacity = 0.2;
                    layer_style.graphicOpacity = 1;
                    OpenLayers.Util.extend(layer_style, style);
                    vecs[i].style = layer_style;
                    vecs[i].ce = ce;
                    CE.MEDIA.features.push(vecs[i]);
                }
                CE.media_layer.addFeatures(vecs);
                CE.LAYERS.refresh();
            }
        },

        // set the lat and long fields to the point selected by the user.
        set_point: function (point) {
            this.$.find('input[name="latitude"]').val(point.y);
            this.$.find('input[name="longitude"]').val(point.x);
        },

        // generate a text field.
        txt: function (name) {
            return $.touch('txt').attr('name', name);
        },

        // draw the form for creating the new media item.
        media_form: function (keep_old_entries) {
            this.create_dialog_content(keep_old_entries, "/upload_media", true);
        },

        create_dialog_content: function (keep_old_entries, form_action, lat_lng) {
            // A select box with all the available media types
            var $select = $.touch('select.theme[name="theme"]');
            $.each(CE.THEME.themes, function (k, theme) {
                var $opt = $.touch('option').val(theme.id).text(theme.title);
                $opt.attr('selected', CE.THEME.active_theme.id == theme.id)
                $select.touch($opt);
            });
            $file = $('<input type="file" name="image" class="image"/>');
            var today = new Date();
            today = today.getFullYear() + '-' + today.getMonth() + '-' + today.getDate();

            var drawPointClick = function () {
                CE.MEDIA.deactivateAllDrawTool();
                CE.controls.draw_tool.activate();
                CE.user_shape_layer.removeFeatures(CE.MEDIA.current_features);
                CE.MEDIA.current_features = [];
                CE.MEDIA.ADD.$iframe.contents().find('input.draw').val('');
                if (CE.MEDIA.selectedFeature)
                    CE.MEDIA.selectedFeature.style.fillColor = CE.MEDIA.selectedFeatureFillColor;
                CE.MEDIA.ADD.$iframe.contents().find('fieldset.latlng').show();
            };
            
            var drawPolygonClick = function () {
                CE.MEDIA.deactivateAllDrawTool();
                CE.MEDIA.polygonTool.activate();
                $('#ce_dialog').hide();
                CE.user_shape_layer.removeFeatures(CE.MEDIA.current_features);
                CE.MEDIA.current_features = [];
                if (CE.MEDIA.selectedFeature)
                    CE.MEDIA.selectedFeature.style.fillColor = CE.MEDIA.selectedFeatureFillColor;
                CE.MEDIA.ADD.$iframe.contents().find('fieldset.latlng').hide();
            };

            var chooseShapeClick = function () {
                CE.MEDIA.deactivateAllDrawTool();
                CE.MEDIA.chooseMode = true;
                $('#ce_dialog').hide();
                CE.user_shape_layer.removeFeatures(CE.MEDIA.current_features);
                CE.MEDIA.current_features = [];
                if (CE.MEDIA.selectedFeature)
                    CE.MEDIA.selectedFeature.style.fillColor = CE.MEDIA.selectedFeatureFillColor;
                CE.MEDIA.ADD.$iframe.contents().find('fieldset.latlng').hide();
            };

            var $type_select = $('<select name="type"><option value=""></option><option value="sound">sound</option><option value="movie">movie</option><option value="photo">photo</option><option value="chart">chart</option><option value="story">story</option></select>');

            this.$ = (keep_old_entries && this.$) || $('<form action="' + form_action + '" method="post" enctype="multipart/form-data" ></form>').touch(
            $('<input type="hidden" name="themes" />').val("[" + CE.THEME.active_theme.id + "]"), ' Title:&nbsp;', this.txt('title').addClass('required'), 'br', ' Description:&nbsp;', 'textarea[name="content"]', 'br', $select, ' &nbsp;Source:&nbsp;', this.txt('source').addClass('required'), ' &nbsp;Type:&nbsp;', $type_select, $.touch('fieldset').touch('legend  Date Range ', this.txt('date_start').val(today), 'span   to ', this.txt('date_end').val(today)), $.touch('fieldset').touch($.touch('legend').touch(' DRAW: ', $.touch('input.pointer').click(drawPointClick).addClass('point').attr('checked', 'checked').attr('type', 'radio').attr('name', 'draw').attr('value', 'point'), ' point', $.touch('input.pointer').click(drawPolygonClick).addClass('polygon').attr('type', 'radio').attr('name', 'draw').attr('value', 'polygon'), ' polygon&nbsp;&nbsp;OR:', $.touch('input.pointer').click(chooseShapeClick).addClass('existing').attr('type', 'radio').attr('name', 'draw').attr('value', ' existing'), ' select an existing shape'), $.touch('div').hide().touch('span  shape: ', this.txt('shape').attr('readonly', 'readonly').attr('size', '78').addClass('draw'))), lat_lng ? $.touch('fieldset').addClass('latlng').touch('legend  Lat/Long Coordinates (click a location on the map)', 'span  lat: ', this.txt('latitude'), 'span  long: ', this.txt('longitude')) : ' ', $.touch('fieldset.image').touch('legend  ' + (lat_lng ? 'Attach a file' : 'Attach a zip file including the following files: .shp, .shx, .dbf, .prj'), $file), $.touch('p').css({
                'text-align': 'right'
            }).touch(
            $.touch('input#save_media.ce-button').attr('type', 'submit').val('Save'), $.touch('input#save_media.ce-button').attr('type', 'submit').val('Save and Add Another...').click(function () {
                CE.MEDIA.ADD.add_another = true; // auto-open a new upload form when done submitting.
            })) // last item
            );

            this.add_another = false; // by default, close the upload dialog after submit.
            $body = this.get_frame_body().css({
                margin: 0,
                padding: 0,
                overflow: 'hidden'
            }).append(this.$);
            this.$iframe.contents().find('head').append('<link rel="stylesheet" type="text/css" href="/media/css/ce.css"/>');

            // get rid of scrollbar.
            this.$iframe.contents().find('body').css({
                margin: 0
            });

            /* configure the datePicker plugin */
            this.$.find(".date_start,.date_end").datePicker({
                verticalOffset: this.$iframe.offset().top,
                horizontalOffset: this.$iframe.offset().left + 100,
                startDate: "1900-01-01"
            }).bind( /* show the calendar when we focus a date input */ 'focus', function () {
                $(this).dpDisplay();
            });
            /* hide the calendar when the iframe is clicked */
            $(this.$iframe.get(0).contentWindow.document).mousedown(function () {
                $(this).find(".date_start,.date_end").dpClose();
            });

            this.$.validate({
                // nothing here required, this just validates the form
            });

            $('.close-button').click(function () {
                CE.MEDIA.deactivateAllDrawTool();
                CE.user_shape_layer.removeFeatures(CE.MEDIA.current_features);
                if (CE.MEDIA.selectedFeature)
                    CE.MEDIA.selectedFeature.style.fillColor = CE.MEDIA.selectedFeatureFillColor;
            });
        }
    },

    remove: function (id) {
        $.post("/delete_media/" + id + "/", function (rsp) {
            rsp = CE.UTIL.parse_json(rsp);
            if (rsp.success) {
                // remove and get rid of all references to the feature.
                for (var i = 0; i < CE.MEDIA.features.length; i++)
                {
                    var feature = CE.MEDIA.features[i];
                    if ('' + feature.ce.id === '' + id) {
                        CE.media_layer.removeFeatures([feature]);
                        CE.MEDIA.features.splice(i, 1);
                        i -= 1;
                    }
                }
                for (var i = 0; i < CE.MEDIA.nsp_features.length; i++)
                {
                    var feature = CE.MEDIA.nsp_features[i];
                    if ('' + feature.ce.id === '' + id) {
                        CE.MEDIA.nsp_features.splice(i, 1);
                        i -= 1;
                    }
                }
                
                CE.WIDGETS.DIALOG.close();
                CE.MEDIA.list.draw();
                CE.WIDGETS.SLIDER.markers.refresh();

            } else {
                alert('failed to delete media: ' + rsp.errMsg);
            }
        });
    },

    // gets the rendered content for a feature.
    renderContent: function (feature) {
        var ce = feature.ce;
        // generate the media window contents.
        var date_text = ce.date_start + (ce.date_end != ce.date_start && ce.date_end != 'None' ? 'to ' + ce.date_end : '');
        var content = ce.content;

        // IE mangles youtube video markup unless we alter it as follows...
        if (content.match(/^\s*<object/)) {
            var m = ce.content.match(/<embed.*?<\/embed>/);
            if (m && m.length) content = m[0];
        }

        return $.touch('div').touch('div.media-content  ' + (ce.type.name === 'chart' ? "<div id='ce_chart'></div>" : content), 'div.ce-date  ' + date_text, 'div.source  ' + CE.UTIL.linkify(ce.source) + (ce.owner_id == CE.user.id ? "<a href='javascript:CE.MEDIA.remove(" + ce.id + ")'>[del]</a>" : ""));
    },

    select_feature: function(feature_id) {
        var feature;
        for (var i = 0; i < CE.MEDIA.features.length; i++)
        {
            if (CE.MEDIA.features[i].id == feature_id)
            {
                feature = CE.MEDIA.features[i];
                break;
            }
        }
        CE.MEDIA.dialog(feature);
    },

    media_list_dialog: function (features) {
        var content = '<ul>';
        for (var i = 0; i < features.length; i++)
        {
            content += '<li><a href="javascript:CE.MEDIA.select_feature(\'' + features[i].id + '\')">' + features[i].ce.title  + '</a></li>'
        }
        content += '</ul>';

        CE.WIDGETS.DIALOG.draw({
            content: content,
            title: 'Media Items',
            width: 450
        });
        
        var pixel = CE.map.getPixelFromLonLat(
        new OpenLayers.LonLat(features[0].geometry.getCentroid().x, features[0].geometry.getCentroid().y));
        // draw a stem object pointing to the media feature
        CE.WIDGETS.DIALOG.stem(pixel.x, pixel.y + CE.$map.offset().top);
    },

    // A window which displays details and rich media pertaining to a media marker on the map.
    dialog: function (feature, opts) {

        opts = $.extend({
            recenter: false
        }, opts);
        // remove highlighting from previously selected media.
        if (CE.selected_feature) {
            $('#label_' + CE.selected_feature.id.replace(/\./g, "-")).add('#marker_' + CE.selected_feature.id.replace(/\./g, "-")).removeClass('active');
        }

        // Get the feature associated with the click.
        CE.selected_feature = feature;
        var ce = feature.ce;

        var content = this.renderContent(feature);

        content.find('.media-content img:not(.icon)').css('width', $.browser.msie ? '500px' : '100%');

        CE.WIDGETS.DIALOG.draw({
            content: content,
            title: ce.title + " - " + ce.date_start.substr(0, 10),
            width: 450,
            on_click: function (evt) {

                var $tgt = $(evt.target);
                // The "back" link for media that 
                // are part of a media collection.
                if ($tgt.hasClass('back')) {
                    CE.MEDIA.dialog(CE.selected_feature);
                } else {

                    // Media collection click handlers.
                    if (!$tgt.hasClass('list-item')) {
                        $tgt = $tgt.parent();
                    }
                    if (!$tgt.hasClass('list-item')) {
                        return;
                    }
                    var idx = $tgt.parent().children().index($tgt);
                    var ce = CE.selected_feature.ce.children[idx].ce;
                    // TODO: refactor this to share rendering capability with CE.media_window.
                    $tgt.parent().parent().empty().append("<img src='/media/img/back.png' class='back'/>").append(CE.MEDIA.renderContent(CE.selected_feature.ce.children[idx]));
                }
            }
        });

        if (feature.geometry) {
            if (opts.recenter && !feature.onScreen()) {
                CE.map.moveTo(new OpenLayers.LonLat(feature.geometry.getCentroid().x, feature.geometry.getCentroid().y), {
                    animate: false
                });
                CE.map.pan(-200, 0, {
                    animate: false
                });
            }
            var pixel = CE.map.getPixelFromLonLat(
            new OpenLayers.LonLat(feature.geometry.getCentroid().x, feature.geometry.getCentroid().y));

            // draw a stem object pointing to the media feature
            CE.WIDGETS.DIALOG.stem(pixel.x, pixel.y + CE.$map.offset().top);
        }

        // If it's a chart, render it.
        if (ce.type.name == 'chart') {
            CE.MEDIA.graph_by_year(CE.UTIL.parse_json(ce.content));
        }

        // highlight the label and time slider marker for this media.
        $('#label_' + feature.id.replace(/\./g, "-")).add('#marker_' + feature.id.replace(/\./g, "-")).addClass('active');
    },

    get_feature_by_id: function (fid) {
        var feature = CE.media_layer.getFeatureById(fid);
        if (!feature) {
            for (var i = 0; i < CE.MEDIA.nsp_features.length; i++) {
                if (fid == CE.MEDIA.nsp_features[i].id) {
                    feature = CE.MEDIA.nsp_features[i];
                }
            }
        }
        return feature;
        //for (var i = 0; i < CE.MEDIA.features; i++) {
        //    if (fid == CE.MEDIA.features[i].id) return CE.MEDIA.features[i];
        //}
    },

    get_feature_by_label: function (label) {
        var fid = label.replace(/\-/g, ".").replace("label_", "");
        return CE.MEDIA.get_feature_by_id(fid);
    },

    //calls the provided function for each media feature, including non-spatial ones.
    iter_all_features: function (fn) {
        for (i = 0; i < CE.MEDIA.features.length; i++) {
            fn(CE.MEDIA.features[i], i);
        }
        for (i = 0; i < CE.MEDIA.nsp_features.length; i++) {
            fn(CE.MEDIA.nsp_features[i], i);
        }
    },

    // Hook into flot.jquery graph library for data visualization.
    graph_by_year: function (opts) {

        opts = $.extend({
            step_year: 1,
            label: 'untitled'
        }, opts);

        opts.end_year = opts.step_year * (opts.data.length - 1) + opts.start_year;

        var max = opts.data[0],
            min = opts.data[0],
            year, idx, d = [];

        for (year = opts.start_year; year <= opts.end_year; year += opts.step_year) {
            idx = year - opts.start_year;
            d.push([year, opts.data[idx]]);
            if (opts.data[idx] < min) min = opts.data[idx];
            if (opts.data[idx] > max) max = opts.data[idx];
        }

        $.plot($("#ce_chart"), [{
            data: d,
            label: opts.label
        }], {
            yaxis: {
                ticks: 5,
                min: min,
                max: max
            },
            xaxis: {
                ticks: 10,
                min: opts.start_year,
                max: opts.end_year
            },
            grid: {
                backgroundColor: {
                    colors: ["#fff", "#eee"]
                }
            },
            lines: {
                show: true
            }
        });
    },

    filter: 'photo',

    // the list of media.
    list: {

        // updates the list after a theme change.
        refresh: function () {

            var $tab = $("#ce_right #ce_tab_media_content p.multimedia-items").find('input').unbind().end() // clean up any left over events (due to watermark).
            .empty();
            var feature_list = [];
            CE.MEDIA.iter_all_features(function (feature) {
                if (CE.THEME.contains(feature) && CE.MEDIA.filter === feature.ce.type.name) {
                    feature_list.push(feature);
                }
            });
            $tab.append(this.draw(feature_list, true)).touch('div.clear', $.touch('p#search_area').touch(
            $.touch('txt#search_box').width(70).Watermark('Search').keyup(function (e) {
                if (e.which === 13) $('#search_media').click();
            }), ' &nbsp;', $.touch('btn#search_media.ce-button').val('Search'), (CE.user.auth) ? $.touch('btn#add_media.ce-button').val('Add...') : 'div'));
        },

        // render a list of features and return it.
        // @param set_visibility : list is time-slider dependent, so setting this.start_year and this.end_year is required.
        draw: function (features, set_visibility) {
            if (typeof set_visibility === 'undefined') set_visibility = true;
            var h = "<div class='feature-list'>",
                feature, item;
            for (var fk in features) {
                if (features.hasOwnProperty(fk)) {
                    feature = features[fk];
                    item = "<div class='wrapper-" + feature.ce.type.name + "' id='label_" + feature.id.replace(/\./g, "-") + "'" + (set_visibility && !CE.MEDIA.is_current(feature, CE.start_year, CE.end_year) ? " style='display:none'" : "") + ">";

                    if (feature.ce.type.name === 'photo') {
                        if (feature.ce.src) {
                            item += "<img src='" + feature.ce.src.replace('.jpg', '.thumbnail.jpg') + "' class='list-tile' />";
                        } else {
                            item += 'missing';
                        }
                    } else {
                        item += "<span class='list-item'>&gt; " + feature.ce.title + "</span>";
                    }

                    h += item + "</div>";
                }
            }
            return h + "</div>";
        }
    },

    is_current: function (feature, start_year, end_year) {
        // no range defined, or within range....
        start_year = start_year || CE.start_year;
        end_year = end_year || CE.end_year;
        is_current = !feature.ce.start_year || !((feature.ce.end_year || 999999) < start_year || (feature.ce.start_year || 0) > end_year) || !CE.WIDGETS.SLIDER.is_visible;
        is_current = is_current && CE.THEME.contains(feature);
        return is_current;
    },

    /* MEDIA.refresh()
     *   adds/removes media features to media layer based on time slider position.
     */
    refresh: function () {
        // this allows us to avoid redundant refreshes when both endpoints are moved.
        if (CE.cancel_next_refresh) {
            CE.cancel_next_refresh = false;
            return;
        }

        var is_current, showing, feature, layer;
        var $feature_list = $(".feature-list");
        layer = CE.media_layer;

        // TODO: lots of optimization.
        for (i = 0; i < CE.MEDIA.features.length; i++) {
            feature = CE.MEDIA.features[i];

            // test if feature exists in the current timeframe defined by the slider.
            is_current = CE.MEDIA.is_current(feature, CE.start_year, CE.end_year);
            showing = feature.layer;

            // the feature isn't yet displayed but should be...
            if (!showing && is_current) {
                layer.addFeatures([feature]);
                if (feature.is_nsp || feature.onScreen()) $feature_list.find(feature.ce.label_id).show();
            }

            // the feature is displayed but shouldn't be...
            if (showing && (!is_current)) {
                if (feature.is_nsp || feature.onScreen()) $feature_list.find(feature.ce.label_id).hide();
                layer.removeFeatures([feature]);
            }

        } // end feature loop
        // update current layers based on timeslider.
        for (i = 0; i < CE.map.layers.length; i++) {
            layer = CE.map.layers[i];
            // test if the layer is dynamic wrt time.
            if (layer.ce && layer.ce.start_year && layer.ce.themes) {
                // test if layer exists in the current timeframe defined by the slider.
                // no range defined, or within range...
                is_current = CE.MEDIA.is_current(layer, CE.start_year, CE.end_year);

                showing = layer.visibility;

                // the layer isn't yet displayed but should be...
                /*if (!showing && is_current) {
                    layer.setVisibility(true);
                    showing = true;
                } else 
                // the layer is displayed but shouldn't be...
                if (showing && (!is_current)) {
                    layer.setVisibility(false);
                }*/

                if (showing && layer.ce.tags.time) {
                    CE.LAYERS.wms.update_time(layer);
                }
            }
        } // end layer loop
    },

    update_all: function () {
        this.refresh();
        this.list.refresh();
        CE.WIDGETS.SLIDER.markers.refresh();
        this.spatial_refresh();
    },

    // an optimized iterator for use by the media refresh loops.
    spatial_refresh: function () {

        var $feature_list = $(".feature-list");
        var start_year = CE.start_year;
        var end_year = CE.end_year;

        CE.MEDIA.iter_all_features(function (feature) {
            if (CE.MEDIA.is_current(feature, start_year, end_year)) {
                $feature_list.find(feature.ce.label_id)[(feature.is_nsp || feature.onScreen() ? 'show' : 'hide')]();
            }
        });

    }

};
